//
//  AliOSSUtil.swift
//  Swift-Codedase
//
//  Created by xyanl on 2023/7/21.
//

import Foundation
import AliyunOSSiOS

enum AliOSSPermission {
    case STS
    case RAM
}

public class AliOSSUtil {
    
    /// bytesSent                 本次要发送的字节数（一个大文件可能要分多次发送）
    /// totalBytesSent            已发送的字节数
    /// totalBytesExpectedToSend  本次上传要发送的总字节数（即一个文件大小）
    typealias AliOSSProgressHandler = ((_ bytesSent: Int64, _ totalBytesSent: Int64, _ totalBytesExpectedToSend: Int64) -> Void)
    typealias AliOSSCompletionHandler = ((_ object: AliOSSObject, _ error: Error?) -> Void)
    typealias AliOSSCompletionsHandler = ((_ objects: [AliOSSObject], _ error: Error?) -> Void)
    
    static let shared: AliOSSUtil = AliOSSUtil()
    
    /// OSS客户端(多个endpoint需要对应多个client，client必须和app生命周期一致，不能是局部变量)
    private var client: OSSClient!
    /// OSS 初始化参数
    private var config: AliOSSConfig!
    /// ClientConfiguration 是 OSSClient 的配置类，您可通过此类来配置代理、连接超时、最大连接数等参数。
    private var clientConfiguration: OSSClientConfiguration!
    
    func initClient(_ permission: AliOSSPermission = .RAM, config: AliOSSConfig, clientConfiguration: OSSClientConfiguration = .default) {
        self.clientConfiguration = clientConfiguration
        self.config = config
        if permission == .RAM {
            initRAMClient()
        } else {
            initSTSClient()
        }
    }
}

//MARK: - 初始化
extension AliOSSUtil {
    /// STS 管理器对象 (STS临时授权访问OSS)
    private func initSTSClient() {
        
        /// 阿里云代理验证，使用上传、下载API时，回调此处验证阿里云federationToken是否过期
        let credential = OSSFederationCredentialProvider(federationTokenGetter: { [self] () -> OSSFederationToken? in
            
            /// 初始化OSSFederationToken
            let stcToken = OSSFederationToken()
            stcToken.tAccessKey = config.accessKey!
            stcToken.tSecretKey = config.secretKey!
            stcToken.tToken = config.securityToken!
            stcToken.expirationTimeInGMTFormat = config.expirationTimeInGMTFormat!
            
            /// 返回stcToken
            return stcToken
            
        }) as OSSFederationCredentialProvider
        
        /// 创建客户端
        client = OSSClient.init(endpoint: config.endpoint, credentialProvider: credential, clientConfiguration: clientConfiguration)
    }
    
    
    /// RAM 管理器对象 (RAM主要的作用是控制账号系统的权限)
    private func initRAMClient() {
        
        let credential: OSSPlainTextAKSKPairCredentialProvider = OSSPlainTextAKSKPairCredentialProvider.init(
            plainTextAccessKey: config.accessKey,
            secretKey: config.secretKey
        )
        
        client = OSSClient.init(
            endpoint: config.endpoint,
            credentialProvider: credential,
            clientConfiguration: clientConfiguration
        )
    }
    
    /// 创建上传请求
    /// - Parameters:
    ///   - object: 上传对象
    ///   - progress: 上传进度
    /// - Returns: description
    private func createPutObjectRequest(object: AliOSSObject, progress: AliOSSProgressHandler? = nil) -> OSSPutObjectRequest {
        // 1.上传请求
        let request = OSSPutObjectRequest()
        // 1.1 设置bucketName (文件夹名，与OSS对应)
        request.bucketName = config.bucketName
        // 1.2 文件名路径
        request.objectKey = config.folderName + "/" + object.name
        // 1.3 上传数据，url和data二选一，优先使用url
        if let url = object.uploadingFileURL {
            request.uploadingFileURL = url
            
        } else if let data = object.uploadingData {
            request.uploadingData = data
        }
        
        if let pro = progress {
            // 1.4 上传进度
            request.uploadProgress = pro
        }
        
        // 配置contentType，参考: https://help.aliyun.com/zh/oss/user-guide/configure-the-content-type-header#concept-5041
        // Content-Type的值与实际的文件类型不一致。浏览器无法正确解析和渲染该文件，而只能将其作为下载文件处理。
        // Content-Type用于指定Object的文件类型。 文件类型决定浏览器将以什么形式、什么编码读取文件。 如果没有指定文件类型，则根据文件的扩展名生成。 如果文件没有扩展名，则使用文件类型的默认值application/octet-stream。
        // request.contentType = ""
        
        return request
    }
    
    /// 上传成功后的网络地址
    /// - Parameter objectKey: 路径+文件名
    private func fileNetworkUrl(objectKey: String) -> String {
        
        if let dm = config.domainName {
            // 使用公司自己的域名
            return dm + "/" + objectKey
        }
        return config.aliDomainName + "/" + objectKey
    }
}

//MARK: - 图片上传
extension AliOSSUtil {
    
    /// 上传单个文件
    /// - Parameters:
    ///   - object: 文件对象
    ///   - progress: 进度
    ///     bytesSent                 本次要发送的字节数（一个大文件可能要分多次发送）
    ///     totalBytesSent            已发送的字节数
    ///     totalBytesExpectedToSend  本次上传要发送的总字节数（即一个文件大小）
    ///   - completion: 结果
    func uploadSingleObject(
        _ object: AliOSSObject,
        progress: AliOSSProgressHandler? = nil,
        completion: @escaping AliOSSCompletionHandler
    ) {
        // 视频需要上传封面
        if object.type == .video && object.isUploadCover {
            uploadVideoCoverObject(object: object, progress: progress, completion: completion)
            return
        }
        
        if object.url.count != 0 {
            // 已经上传过了
            xLog("已经上传过了 \(object.url)")
            completion(object, nil)
        }
        
        let request = createPutObjectRequest(object: object, progress: progress)
        let task = client.putObject(request)
        
        task.continue(successBlock: { [self] (aTask: OSSTask) -> Any? in
            
            if aTask.error == nil {
                // 上传成功
                object.url = fileNetworkUrl(objectKey: request.objectKey)
                // xLog("上传成功 \(object.url)")
                
            } else {
                object.url = ""
                object.coverUrl = nil
            }
            
            completion(object, aTask.error)
            
            return nil
        })
    }
    
    
    /// 上传视频和封面
    /// - Parameters:
    ///   - object: 文件
    ///   - progress: 进度
    ///   - completion: 结果
    private func uploadVideoCoverObject(
        object: AliOSSObject,
        progress: AliOSSProgressHandler? = nil,
        completion: @escaping AliOSSCompletionHandler
    ) {
        // 文件总大小
        let totalBytes: Int64 = Int64((object.uploadingData?.count ?? 0) + (object.coverData?.count ?? 0))
        // 当前已发送的总数
        var currentTotalBytesSent: Int64 = 0
        
        var error: Error?
        // 创建 工作组
        let group = DispatchGroup()
        // 创建队列
        let queue = DispatchQueue(label: "workingGroup", attributes: .concurrent)
        
        // 第1个网络请求
        group.enter()
        queue.async { [self] in
            // 上传视频
            let request = createPutObjectRequest(object: object) { bytesSent, totalBytesSent, totalBytesExpectedToSend in
                currentTotalBytesSent = currentTotalBytesSent + bytesSent
                progress?(bytesSent, totalBytesSent, totalBytes)
            }
            
            client.putObject(request).continue(successBlock: { [self] (aTask: OSSTask) -> Any? in
                
                if aTask.error == nil {
                    // 上传成功
                    let url = fileNetworkUrl(objectKey: request.objectKey)
                    object.url = url
                    xLog("视频上传成功 \(url)")
                    
                } else {
                    object.url = ""
                    object.coverUrl = ""
                    error = aTask.error
                }
                
                // 请求结束退出
                group.leave()
                
                return nil
            })
        }
        
        // 第2个网络请求
        group.enter()
        queue.async { [self] in
            // 上传封面
            let request = createPutObjectRequest(object: object) { bytesSent, totalBytesSent, totalBytesExpectedToSend in
                currentTotalBytesSent = currentTotalBytesSent + bytesSent
                progress?(bytesSent, totalBytesSent, totalBytes)
            }
            // 上传封面数据
            request.uploadingData = object.coverData!
            // 重新设置封面路径
            request.objectKey = config.folderName + "/" + object.coverName!
            // 阿里云OSS上传图片不能打开的解决办法
            // Content-Type用于指定Object的文件类型。
            // 文件类型决定浏览器将以什么形式、什么编码读取文件。
            // 如果没有指定文件类型，则根据文件的扩展名生成。
            // 如果文件没有扩展名，则使用文件类型的默认值application/octet-stream。
            request.contentType = "image/png"
            
            client.putObject(request).continue(successBlock: { [self] (aTask: OSSTask) -> Any? in
                
                if aTask.error == nil {
                    // 上传成功
                    let url = fileNetworkUrl(objectKey: request.objectKey)
                    object.coverUrl = url
                    xLog("封面上传成功 \(url)")
                    
                } else {
                    object.url = ""
                    object.coverUrl = ""
                    error = aTask.error
                }
                
                // 请求结束退出
                group.leave()
                
                return nil
            })
        }
        
        group.notify(queue: DispatchQueue.main) {
            // 都上传完成
            completion(object, error)
        }
    }
    
    
    /// 同时上传多个文件
    /// - Parameters:
    ///   - objects: 文件数组
    ///   - progress: 进度
    ///   - completion: completion description
    func uploadManyObject(
        objects: [AliOSSObject],
        progress: AliOSSProgressHandler? = nil,
        completion: @escaping AliOSSCompletionsHandler
    ) {
        
        var aError: Error?
        // 创建 工作组
        let group = DispatchGroup()
        // 创建队列
        let queue = DispatchQueue(label: "workingGroup", attributes: .concurrent)
        
        for object in objects {
            
            // 第1个网络请求
            group.enter()
            queue.async { [self] in
                uploadSingleObject(object, progress: progress) { object, error in
                    if error != nil {
                        aError = error
                    }
                    // 请求结束退出
                    group.leave()
                }
                
            }
        }
        
        group.notify(queue: DispatchQueue.main) {
            // 都上传完成
            completion(objects, aError)
        }
        
    }
    

    // MARK: - Private
    
    
    // MARK: - 分片上传
    // 通常在文件大于100 MB的情况下，建议采用分片上传的方法，通过断点续传和重试，提高上传成功率。如果在文件小于100 MB的情况下使用分片上传，且partSize设置不合理的情况下，可能会出现无法完整显示上传进度的情况。对于小于100 MB的文件，建议使用简单上传的方式。
    func multipartUpload(object: AliOSSObject) {
        let request = OSSMultipartUploadRequest()
        // request.contentType = @"application/octet-stream"
        request.uploadingFileURL = object.uploadingFileURL!
        request.bucketName = config.bucketName
        request.objectKey = config.folderName + "/" + object.name
        // request.partSize = 1024*1024
        request.uploadProgress = { (bytesSent, totoalBytesSent, totalBytesExpectedToSent) -> Void in
            let progress = Double(totoalBytesSent) / Double(totalBytesExpectedToSent)
            xLog("[上传] 图片上传进度 \(Int(progress*100)), \(Int(bytesSent))")
        }
        
        let task = client.multipartUpload(request)
        task.continue({ [self] (t) -> Any? in
            object.url = fileNetworkUrl(objectKey: request.objectKey)
            xLog("上传成功:" + object.url)
            
            return nil
        }).waitUntilFinished()
        
        
    }
    
}

extension OSSClientConfiguration {
    
    public static var `default`: OSSClientConfiguration {
        let config = OSSClientConfiguration()
        // 最大重试次数
        config.maxRetryCount = 3
        // 请求超时时间
        config.timeoutIntervalForRequest = 60
        // 设置 单个object下载最长持续时间
        config.timeoutIntervalForResource = TimeInterval(24 * 60 * 60)
        // 最大并发请求数
        config.maxConcurrentRequestCount = 60
        // 设置 是否开启后台传输
        config.enableBackgroundTransmitService = false
        
        return config
    }
    
}
